Vue路由懒加载原理及实现

您所在的位置:网站首页 vue router 懒加载 Vue路由懒加载原理及实现

Vue路由懒加载原理及实现

2023-04-07 04:22| 来源: 网络整理| 查看: 265

本文通过搭建一个Vue的简单项目,引入了Vue-router组件,从页面的3个文件入手,详细介绍了怎么加载chunk和怎么执行module,从而介绍整个路由组件懒加载的过程,希望对你有帮助。

前言

说起路由懒加载,大家很快就知道怎么实现它,但是问到路由懒加载的原理,怕有一部分小伙伴是一头雾水了吧。下面带大家一起去理解路由懒加载的原理。 路由懒加载也可以叫做路由组件懒加载,最常用的是通过​​import()​​来实现它。

function load(component) { return () => import(`views/${component}`)}

然后通过Webpack编译打包后,会把每个路由组件的代码分割成一一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。 在这里先不管Webpack是怎么按路由组件分割代码,只管在Webpack编译后,怎么实现按需加载对应的路由组件js文件。

一、准备工作1、搭建项目

想要理解路由懒加载的原理,建议从最简单的项目开始,用Vue Cli3搭建一个项目,其中只包含一个路由组件。在main.js只引入vue-router,其它统统不要。 main.js

import Vue from 'vue';import App from './App.vue';import Router from 'vue-router';Vue.use(Router);//路由懒加载function load(component) { return () => import(`views/${component}`)}// 路由配置const router = new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: load('Home'), meta: { title: '首页' } }, ]});new Vue({ router, render: h => h(App)}).$mount('#app')

views/Home.vue

{{tip}} export default { data(){ return { tip:'欢迎使用Vue项目' } }}2、webpackChunkName

利用​​webpackChunkName​​,使编译打包后的js文件名字能和路由组件一一对应,修改一下load函数。

function load(component) { return () => import(/* webpackChunkName: "[request]" */ `views/${component}`)}3、去掉代码压缩混淆

去掉代码压缩混淆,便于我们阅读编译打包后的代码。在vue.config.js中配置

module.exports={ chainWebpack:config => { config.optimization.minimize(false); },}4、npm run build

执行命令​​npm run build​​​,编译打包后的dist文件结构如下所示 Vue路由懒加载原理及实现_懒加载 其中Home.67f3cd34.js就是路由组件Home.vue编译打包后对应的js文件。

二、分析index.html

Vue路由懒加载原理及实现_懒加载_02 从上面我们可以看到,先用link定义Home.js、app.js、chunk-vendors.js这些资源和web客户端的关系。

​​ref=preload​​:告诉浏览器这个资源要给我提前加载。​​rel=prefetch​​:告诉浏览器这个资源空闲的时候给我加载一下。​​as=script​​:告诉浏览器这个资源是script,提升加载的优先级。

然后在body里面加载了chunk-vendors.js、app.js这两个js资源。可以看出web客户端初始化时候就加载了这个两个js资源。

三、分析chunk-vendors.js

chunk-vendors.js可以称为项目公共模块集合,代码精简后如下所示,

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-vendors"],{ "01f9":(function(module,exports,__webpack_require__){ ...//省略 }) ...//省略}])

从代码中可以看出,执行chunk-vendors.js,仅仅把下面这个数组​​push​​​到​​window["webpackJsonp"]​​​中,而数组第二项是个对象,对象的每个value值是一个函数表达式,不会执行。就这样结束了,当然不是,我们带着​​window["webpackJsonp"]​​去app.js中找找。

四、分析app.js

app.js可以称为项目的入口文件。 app.js里面是一个自执行函数,通过搜索​​window["webpackJsonp"]​​可以找到如下相关代码。

(function(modules){ //省略... var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); var parentJsonpFunction = oldJsonpFunction; //省略...}({ 0:(function(module, exports, __webpack_require__) { module.exports = __webpack_require__("56d7"); }) //省略...}))先把​​window["webpackJsonp"]​​​赋值给​​jsonpArray​​。把​​jsonpArray​​​的​​push​​​方法赋值给​​oldJsonpFunction​​。用​​webpackJsonpCallback​​​函数拦截​​jsopArray​​​的​​push​​​方法,也就是说调用​​window["webpackJsonp"]​​​的​​push​​​方法都会执行​​webpackJsonpCallback​​函数。将​​jsonpArray​​​浅拷贝一下再赋值给​​jsonpArray​​。因为执行chunk-vendors.js中的​​window["webpackJsonp"].push​​​时​​push​​​方法还未被​​webpackJsonpCallback​​​函数拦截,所以要循环​​jsonpArray​​​,将每项作为参数传入​​webpackJsonpCallback​​函数并调用。将​​jsonpArray​​​的​​push​​​方法再赋值给​​parentJsonpFunction​​。1、webpackJsonpCallback函数

接下来我们看一下​​webpackJsonpCallback​​这个函数。

(function(modules){ function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var executeModules = data[2]; var moduleId, chunkId, i = 0, resolves = []; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) { resolves.shift()(); } deferredModules.push.apply(deferredModules, executeModules || []); return checkDeferredModules(); }; var installedChunks = { "app": 0 }; //省略...}({ 0:(function(module, exports, __webpack_require__) { module.exports = __webpack_require__("56d7"); }) //省略...}))

想知道​​webpackJsonpCallback​​​函数有什么作用,要先弄明白​​modules​​​、​​installedChunks​​​、​​deferredModules​​这三个变量的作用。

module是指任意的代码块,chunk是webpack处理过程中被分组的module的合集。​​modules​​​缓存所有的module(代码块),调用​​modules​​中的module就可以执行里面的代码。​​installedChunks​​​缓存所有chunk的加载状态,如果​​installedChunks[chunk]​​为0,代表chunk已经加载完毕。​​deferredModules​​​中每项也是一个数组,例如​​[module,chunk1,chunk2,chunk3]​​,其作用是如果要执行module,必须在chunk1、chunk2、chunk3都加载完毕后才能执行。

​​if (parentJsonpFunction) parentJsonpFunction(data)​​​这句代码在多入口项目中才有作用,在前面提到过​​jsonpArray​​​的​​push​​​方法被赋值给​​parentJsonpFunction​​​,调用​​parentJsonpFunction​​​是真正把chunk中push方法中的参数push到​​window["webpackJsonp"]​​​这个数组中。 比如说现在项目有两个入口,app.js和app1.js,app.js中缓存一些module,在app1.js就可以通过​​window["webpackJsonp"]​​来调用这些module,调用代码如下。

for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);

再来理解​​webpackJsonpCallback​​​函数是不是清楚了很多,接下来看一下​​checkDeferredModules​​这个函数。

2、checkDeferredModules函数var deferredModules = [];var installedChunks = { "app": 0}function checkDeferredModules() { var result; for (var i = 0; i < deferredModules.length; i++) { var deferredModule = deferredModules[i]; var fulfilled = true; for (var j = 1; j < deferredModule.length; j++) { var depId = deferredModule[j]; if (installedChunks[depId] !== 0) fulfilled = false; } if (fulfilled) { deferredModules.splice(i--, 1); result = __webpack_require__(__webpack_require__.s = deferredModule[0]); } } return result;}循环​​deferredModules​​​,创建变量​​fulfilled​​​表示​​deferredModule​​​中的chunk加载情况,​​true​​​表示全部加载完毕,​​false​​表示未全部加载完毕。从​​j=1​​​开始循环​​deferredModule​​​中的chunk,因为​​deferredModule[0]​​​是module,如果​​installedChunks[chunk]!==0​​​,则这个chunk未加载完毕,把变量​​fulfilled​​​设置为​​false​​。循环结束后返回result。经循环​​deferredModule​​​中的chunk并判断chunk的加载状态后,​​fulfilled​​​还是为true,则调用​​__webpack_require__​​​函数,将​​deferredModule[0]​​(module)作为参数传入执行。​​deferredModules.splice(i--, 1)​​​,删除满足条件的deferredModule,并将i减一,其中​​i--​​是先使用i,然后在减一。

因为在​​webpackJsonpCallback​​​函数中​​deferredModules​​​为​​[]​​,所以回到主体函数继续往下看。

deferredModules.push([0, "chunk-vendors"]);return checkDeferredModules();

按上面逻辑分析后,会执行​​__webpack_require__(0)​​​,那么来看一下​​__webpack_require__​​这个函数。

3、__webpack_require__函数var installedModules = {};function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l = true; return module.exports;}

从代码可知​​__webpack_require__​​就是一个执行module的方法。

​​installedModules​​用来缓存module的执行状态。通过moduleId在modules(在​​webpackJsonpCallback​​函数中缓存所有module的集合)获取对应的module用call方法执行。将执行结果赋值到module.exports并返回。

所以执行​​__webpack_require__(0)​​,其实就是执行下面的代码。

(function (module, exports, __webpack_require__) { module.exports = __webpack_require__("56d7");}),

在里面又用​​__webpack_require__​​执行id为56d7的module,我们找到对应的module继续看,看一下里面关键的代码片段。

function load(component) { return function () { return __webpack_require__("9dac")("./".concat(component)); };}var routes = [{ path: '/', name: 'home', component: load('Home'), meta: { title: '首页' }}, { path: '*', redirect: { path: '/' }}];

看到这里是不是非常熟悉了,就是配置路由的地方。​​load​​​还是作为加载路由组件的函数,里面用​​__webpack_require__("9dac")​​​返回的方法来执行加载路由组件,我们来看一下​​__webpack_require__("9dac")​​。

(function (module, exports, __webpack_require__) { var map = { "./Home": [ "bb51", "Home" ], "./Home.vue": [ "bb51", "Home" ] }; function webpackAsyncContext(req) { if (!__webpack_require__.o(map, req)) { return Promise.resolve().then(function () { var e = new Error("Cannot find module '" + req + "'"); e.code = 'MODULE_NOT_FOUND'; throw e; }); } var ids = map[req], id = ids[0]; return __webpack_require__.e(ids[1]).then(function () { return __webpack_require__(id); }); } webpackAsyncContext.keys = function webpackAsyncContextKeys() { return Object.keys(map); }; webpackAsyncContext.id = "9dac"; module.exports = webpackAsyncContext;})4、webpackAsyncContext函数

其中的关键函数为​​webpackAsyncContext​​​,调用​​load('Home')​​​时,​​req​​​为​​'./Home'​​​,​​__webpack_require__.o​​方法为

__webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property);};

这个方法就是判断在变量​​map​​​中有没有key为​​./Home​​​的项,如果没有抛出​​Cannot find module './Home'​​​的错误。有执行​​__webpack_require__.e​​​方法,参数为​​Home​​。

5、__webpack_require__.e方法var installedChunks = { "app": 0}__webpack_require__.p = "/";function jsonpScriptSrc(chunkId) { return __webpack_require__.p + "js/" + ({ "Home": "Home" }[chunkId] || chunkId) + "." + { "Home": "37ee624e" }[chunkId] + ".js"}__webpack_require__.e = function requireEnsure(chunkId) { var promises = []; var installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0) { if (installedChunkData) { promises.push(installedChunkData[2]); } else { var promise = new Promise(function (resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise); var script = document.createElement('script'); var onScriptComplete; script.charset = 'utf-8'; script.timeout = 120; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = jsonpScriptSrc(chunkId); var error = new Error(); onScriptComplete = function (event) { // 避免IE内存泄漏。 script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if (chunk !== 0) { if (chunk) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; error.message = 'Loading chunk ' + chunkId + ' failed.n(' + errorType + ': ' + realSrc + ')'; error.name = 'ChunkLoadError'; error.type = errorType; error.request = realSrc; chunk[1](error); } installedChunks[chunkId] = undefined; } }; var timeout = setTimeout(function () { onScriptComplete({ type: 'timeout', target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; document.head.appendChild(script); } } return Promise.all(promises);};

​​__webpack_require__.e​​方法是实现懒加载的核心,在这个方法里面处理了三件事情。

使用JSONP模式加载路由对应的js文件,也可以称为chunk。设置chunk加载的三种状态并缓存在​​installedChunks​​中,防止chunk重复加载。处理chunk加载超时和加载出错的场景。

chunk加载的三种状态

​​installedChunks[chunkId]​​​为​​0​​,代表该chunk已经加载完毕。​​installedChunks[chunkId]​​​为​​undefined​​,代表该chunk加载失败、加载超时、从未加载过。​​installedChunks[chunkId]​​​为​​Promise​​对象,代表该chunk正在加载。

chunk加载超时处理

script.timeout = 120;var timeout = setTimeout(function () { onScriptComplete({ type: 'timeout', target: script });}, 120000);

​​script.timeout = 120​​​代表该chunk加载120秒后还没加载完毕则超时。 用​​setTimeout​​​设置个120秒的计时器,在120秒后执行​​onScriptComplete({ type: 'timeout', target: script })​​​。 在看一下​​onScriptComplete​​函数

var onScriptComplete = function (event) { // 避免IE内存泄漏。 script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if (chunk !== 0) { if (chunk) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; error.message = 'Loading chunk ' + chunkId + ' failed.n(' + errorType + ': ' + realSrc + ')'; error.name = 'ChunkLoadError'; error.type = errorType; error.request = realSrc; chunk[1](error); } installedChunks[chunkId] = undefined; }};

此时chunkId为​​Home​​,加载是Home.js,代码是

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["Home"],{ "bb51":(function(module, __webpack_exports__, __webpack_require__){ //省略... })}]))

在前面有提到​​window["webpackJsonp"]​​​的push方法被​​webpackJsonpCallback​​​函数拦截了,如果Home.js加载成功会自动执行,随后会执行​​webpackJsonpCallback​​​函数,其中有​​installedChunks[chunkId] = 0;​​​会把​​installedChunks['Home']​​​的值置为0。 也就是说,如果Home.js加载超时了,就不能执行,就不能将​​installedChunks['Home']​​​的值置为0,所以此时​​installedChunks['Home']​​​的值还是​​Promise​​​对象。那么就会进入以下代码执行,最后​​chunk[1](error)​​将错误抛出去。

var chunk = installedChunks[chunkId];if(chunk!==0){ if(chunk){ var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; error.message = 'Loading chunk ' + chunkId + ' failed.n(' + errorType + ': ' + realSrc + ')'; error.name = 'ChunkLoadError'; error.type = errorType; error.request = realSrc; chunk[1](error); }}

​​chunk[1]​​其实就是reject函数,在以下代码中给它赋值的。

var promise = new Promise(function (resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject];});

chunk加载失败处理 加载失败分为两种情况,一是Home.js资源加载失败,二是资源加载成功了,但是执行Home.js里面代码出错了导致失败,所以chunk加载失败处理的代码要这么写

script.onerror = script.onload = onScriptComplete;

后面处理的方式和处理加载超时的一样。 ​​__webpack_require__.e​​​最后返回是一个​​Promise​​​对象。回到​​webpackAsyncContext​​函数中

return __webpack_require__.e(ids[1]).then(function () { return __webpack_require__(id);});

​​__webpack_require__.e(ids[1])​​​执行成功后,执行​​ __webpack_require__(id);​​​,此时id为bb51。那么又回到​​__webpack_require__​​​函数中了。在前面提过​​__webpack_require__​​​函数的作用就是执行module。id为bb51的nodule是在Home.js内,在​​webpackJsonpCallback​​函数有以下代码

function webpackJsonpCallback(data) { var moreModules = data[1]; for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } }}五、分析Home.js

Home.js

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["Home"],{ "bb51":(function(module, __webpack_exports__, __webpack_require__){ //省略... })}]))

可以看出moreModules就是​​{"bb51":(function(module, __webpack_exports__, __webpack_require__){})}​​​, 循环moreModules,把Home.js里面的module缓存到app.js里面的modules中。 再看​​__webpack_require__​​函数中有这段代码

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

这样就执行了Home.js里面的module,在module里面有渲染页面的一系列的方法,就把Home.vue这个路由组件页面渲染出来了。 到这里路由组件懒加载的整个流程就结束了,也详细介绍了怎么加载chunk和怎么执行module。

Vue路由懒加载原理及实现_懒加载_03​文章就分享到这,欢迎关注“前端大神之路”​Vue路由懒加载原理及实现_懒加载_03

Vue路由懒加载原理及实现_懒加载_05



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3